import threading
import time
from datetime import datetime, timedelta
from constant import SUCCESS, RUNNING, FAILURE, CRAWL_TYPE_REPO, CRAWL_TYPE_DOC, WARNING, WARNING_SUCCESS
from logger import logger
from store import tasks
import requests
import json
import re
import os
import urllib.parse

"""
语雀爬虫类
该类继承了threading.Thread类，用于实现多线程爬取知识库和文档
"""
class YuqueCrawler(threading.Thread):
    def __init__(self, url, task_id, crawl_type):
        super().__init__()
        # 知识库ID
        self.book_id = None
        # 爬取的知识库/文档 URL
        self.url = url
        # 任务ID
        self.task_id = task_id
        # 爬取类型
        self.crawl_type = crawl_type
        # 任务进度
        self.progress = 0
        # 在tasks字典中初始化任务
        tasks[task_id] = {
                          # 爬取类型
                          'crawlType': crawl_type,
                          # 任务状态
                          'status': RUNNING,
                          # 进度条
                          'progress': self.progress,
                          # 损失任务数
                          'lostMissions': 0,
                          # 知识库ID
                          'bookId': 0}
        # 如果爬取类型为“爬取知识库”，则在tasks字典中初始化doc_tree字段
        if crawl_type is CRAWL_TYPE_REPO:
            tasks[task_id]['docTree'] = []
        else:  # 如果爬取类型为“爬取单文档”，则在tasks字典中初始化doc_title字段
            tasks[task_id]['docTitle'] = ''

    # 保存知识库文档
    def save_repo_page(self, book_id : str, sulg: str, path: str) -> bool:
        # 通过语雀核心API接口获取文档markdown数据
        docsdata = requests.get(
            'https://www.yuque.com/api/docs/' + sulg + '?book_id=' + book_id + '&merge_dynamic_data=false&mode=markdown')
        # 如果请求失败，则打印错误信息
        if docsdata.status_code != 200:
            logger.error(f"文档下载失败 页面可能被删除 {book_id} {sulg} {docsdata.content}")
            # 抛出异常
            return False
        # 将获取到的文档数据转换为json格式，以便于获取文档内容
        docsjson = docsdata.json()
        # 如果下载路径不存在，则创建该路径，并将其作为md文件保存路径
        if not os.path.exists(os.path.dirname(path)):
            os.makedirs(os.path.dirname(path))
        # 将文档内容写入到创建的md文件中
        with open(path, 'w', encoding='utf-8') as f:
            # docsjson.data.sourcecode 为实际的markdown文档内容，直接写入即可
            f.write(docsjson['data']['sourcecode'])
        return True

    # 保存单文档
    def save_doc_page(self, book_id : str, sulg: str, path: str) -> None:
        # 通过语雀核心API接口获取文档markdown数据
        docsdata = requests.get(
            'https://www.yuque.com/api/docs/' + sulg + '?book_id=' + book_id + '&merge_dynamic_data=false&mode=markdown')
        # 如果请求失败，则打印错误信息
        print('https://www.yuque.com/api/docs/' + sulg + '?book_id=' + book_id + '&merge_dynamic_data=false&mode=markdown')
        if docsdata.status_code != 200:
            logger.error(f"文档下载失败 页面可能被删除 {book_id} {sulg} {docsdata.content}")
            # 抛出异常
            raise Exception("文档下载失败 页面可能被删除 ", book_id, sulg, docsdata.content)
        # 将获取到的文档数据转换为json格式，以便于获取文档内容
        docsjson = docsdata.json()
        # 如果下载路径不存在，则创建该路径，并将其作为md文件保存路径
        if not os.path.exists(os.path.dirname(path)):
            os.makedirs(os.path.dirname(path))
        # 将文档内容写入到创建的md文件中
        with open(path, 'w', encoding='utf-8') as f:
            # docsjson.data.sourcecode 为实际的markdown文档内容，直接写入即可
            f.write(docsjson['data']['sourcecode'])

    # 获取文档信息 - json列表数据
    def get_doc_json_data(self):
        # 发送请求获取页面源代码
        docsdata = requests.get(self.url)
        # 从页面源代码中通过正则表达式匹配提取有关知识库ID和文档ID的json数据
        data = re.findall(r"decodeURIComponent\(\"(.+)\"\)\);", docsdata.content.decode('utf-8'))
        # 将json数据解码并返回
        return json.loads(urllib.parse.unquote(data[0]))

    # 爬取知识库
    def crawl_repo(self):
        try:
            # 获取文档信息 - json列表数据
            docsjson = self.get_doc_json_data()
            logger.info(f"任务ID{self.task_id} 开始爬取知识库 {docsjson['book']}")
            # 从json数据中提取知识库ID，并保存到tasks字典中所对应的task中
            self.book_id = str(docsjson['book']['id'])
            # 保存文档所属知识库ID到tasks字典中所对应的task中
            tasks[self.task_id]['book_id'] = self.book_id
            tasks[self.task_id]['taskId'] = self.task_id
            tasks[self.task_id]['bookName'] = docsjson['book']['name']
            # md文档保存目录
            save_dir = f"download/{self.task_id}"
            # 防止目录存在，如果数据保存目录存在，则删除该目录下的所有文件和文件夹
            if os.path.exists(save_dir):
                for root, dirs, files in os.walk(save_dir, topdown=False):
                    for file in files:
                        os.remove(os.path.join(root, file))
                    for dir in dirs:
                        os.rmdir(os.path.join(root, dir))
            else:  # 如果数据保存目录不存在，则创建该目录
                os.makedirs(save_dir)
            # 获取文档总数
            total_docs = len(docsjson['book']['toc'])
            # 已处理/已下载文档数
            processed_docs = 0
            # 从docsjson这个json数组中遍历文档，下载文档
            for doc in docsjson['book']['toc']:
                logger.info(f"任务ID{self.task_id} 开始提取 {doc}")
                # 如果doc对象有url属性，就表示该文档是有实际markdown内容，不是单纯的一个分组，则下载文档
                download_status = True
                if doc['url']:
                    # 保存文档到服务端本地
                    download_status = self.save_repo_page(
                        self.book_id,
                        str(doc['id']),
                        save_dir + "/" + re.sub(r'[<>:"/\\|?*]', '_', doc['title']) + '.md'
                    )
                # 如果只要有一个文件没有成功下载，则设置任务状态为“警告”，并增加损失任务数
                if not download_status:
                    tasks[self.task_id]['status'] = WARNING
                    tasks[self.task_id]['lostMissions'] += 1
                # 更新已处理/已下载文档数
                processed_docs += 1
                # 计算任务进度
                progress = int((processed_docs / total_docs) * 100)
                tasks[self.task_id]['progress'] = progress
                # 保存文档树列表到tasks字典中所对应的task中
                tasks[self.task_id]['docTree'].append({'docId': doc['doc_id'], # 文档ID
                                                        'uuid': doc['uuid'], # 文档UUID
                                                        'prevUUID': doc['prev_uuid'], # 前一个文档ID
                                                        'parentId': doc['parent_uuid'], # 父级文档ID
                                                        'level': doc['level'], # 深度
                                                        'type': doc['type'], # DOC: 文档 TITLE: 分组
                                                        'url': self.url + '/' + doc['url'],  # 文档URL访问链接
                                                        'childUUIDs': doc['child_uuid'], # 子文档UUID
                                                        'title': doc['title'].encode('utf-8').decode('utf-8'), # 文档标题
                                                        'status': download_status # True: 已下载 False: 未下载
                                                        })
                logger.info(f"任务ID{self.task_id} 已下载 {processed_docs}/{total_docs} 篇文档")
                # 如果已处理/已下载文档数等于文档总数，则打印文档下载完成信息
                if processed_docs == total_docs:
                    logger.info(f"任务ID{self.task_id} 文档下载完成，共下载 {total_docs} 篇文档")
                # 模拟下载过程的延时，防止请求过于频繁，以被服务器封禁
                time.sleep(0.1)
            if processed_docs == total_docs:
                if tasks[self.task_id]['lostMissions'] == 0:
                    # 设置任务进度为100，标记任务状态为“完成”
                    tasks[self.task_id]['status'] = SUCCESS
                else:
                    # 设置任务状态为“警告”
                    tasks[self.task_id]['status'] = WARNING_SUCCESS
            # 设置过期时间，方便定时清理
            tasks[self.task_id]['expiryTime'] = datetime.now() + timedelta(minutes=1)
        except BaseException as e:
            logger.error(f"任务ID{self.task_id} 爬取失败: {self.url}, 错误信息: {e}")
            # 如果出错了，则设置任务状态为“失败”，通知前端终止轮询和开始新的任务提交
            tasks[self.task_id]['status'] = FAILURE

    # 爬取单文档
    def crawl_doc(self):
        try:
            docsjson = self.get_doc_json_data()
            # 获取知识库ID
            self.book_id = str(docsjson['book']['id'])
            # 获取文档标题
            title = str(docsjson['doc']['title'])
            # 保存文档所属知识库ID到tasks字典中所对应的task中
            tasks[self.task_id]['bookId'] = self.book_id
            tasks[self.task_id]['bookName'] = docsjson['book']['name']
            # 保存文档标题到tasks字典中所对应的task中
            tasks[self.task_id]['docTitle'] = title
            # md文档保存目录
            save_dir = f"download/{self.task_id}"
            # 防止目录存在，如果数据保存目录存在，则删除该目录下的所有文件和文件夹
            if os.path.exists(save_dir):
                for root, dirs, files in os.walk(save_dir, topdown=False):
                    for file in files:
                        os.remove(os.path.join(root, file))
                    for dir in dirs:
                        os.rmdir(os.path.join(root, dir))
            else:  # 如果数据保存目录不存在，则创建该目录
                os.makedirs(save_dir)
            # 保存文档到服务端本地
            self.save_doc_page(
                self.book_id,
                str(docsjson['doc']['id']),
                save_dir + "/" + re.sub(r'[<>:"/\\|?*]', '_', title) + '.md'
            )
            logger.info(f"任务ID{self.task_id} 已下载 {title}")
            # 设置任务进度为100，标记任务状态为“完成”
            tasks[self.task_id]['progress'] = 100
            tasks[self.task_id]['status'] = SUCCESS
            # 设置过期时间，方便定时清理
            tasks[self.task_id]['expiryTime'] = datetime.now() + timedelta(minutes=1)
        except BaseException as e:
            logger.error(f"任务ID{self.task_id} 爬取失败: {self.url}, 错误信息: {e}")
            # 如果出错了，则设置任务状态为“失败”，通知前端终止轮询和开始新的任务提交
            tasks[self.task_id]['status'] = FAILURE

    # 运行爬虫线程
    def run(self):
        # 如果爬取类型为“爬取知识库”，则调用crawl_repo()方法
        if self.crawl_type == CRAWL_TYPE_REPO:
            self.crawl_repo()
        # 如果爬取类型为“爬取单文档”，则调用crawl_doc()方法
        elif self.crawl_type == CRAWL_TYPE_DOC:
            self.crawl_doc()
